Skip to content

fix(security): report the CSP floor before enforcing it - #3455

Merged
kojiwakayama merged 5 commits into
mainfrom
fix/csp-floor-report-only-until-opt-in
Aug 7, 2026
Merged

fix(security): report the CSP floor before enforcing it#3455
kojiwakayama merged 5 commits into
mainfrom
fix/csp-floor-report-only-until-opt-in

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What

Serve the platform CSP floor as Content-Security-Policy-Report-Only until a project opts in by declaring security.csp. Projects that have declared it keep the enforced header.

Why

#3417 shipped the floor enforced. A fleet audit finds ~100 hosted projects whose assets it blocks — stock photography (images.unsplash.com, i.pravatar.cc, picsum.photos), video (player.vimeo.com, vjs.zencdn.net), object storage (storage.googleapis.com), embeds (www.youtube.com) — including veryfront's own marketing site. Only two projects, codersociety and tomcode, declare security.csp at all.

The owner gets no signal beyond a browser console, and no remedy short of a republish: security.csp lives in project config, and a hosted project serves config from its deployed release, so editing it changes nothing until the next publish. codersociety hit exactly this — added the three directives it needed at 04:57, still served the bare floor hours later because its production release was cut in June. Its entire video library was dead in production.

Behaviour

Project state Header
declares security.csp (incl. csp: {}) Content-Security-Policy
anything else Content-Security-Policy-Report-Only
VERYFRONT_CSP_ENFORCE set Content-Security-Policy (all projects)
VERYFRONT_CSP override set Content-Security-Policy

The reported policy is byte-identical to the enforced one — there is a test pinning that, so what a project sees now is exactly what it gets when the flag flips.

csp: {} counts as opt-in: the signal is that the project touched the key, not how much it put in it. Reading emptiness as "unconfigured" would strand a project that deliberately accepted the floor in report-only forever.

Trade-off

This does relax a security control, and reviewers should weigh that. The floor was enforced for roughly one day, so report-only restores the state the fleet was already in rather than weakening a settled protection — and it converts a silent fleet-wide breakage into visible violation reports. VERYFRONT_CSP_ENFORCE ends the rollout whenever adoption justifies it.

Notes

content-security-policy-report-only joins SECURITY_POLICY_RESPONSE_HEADER_NAMES, so a project-provided value cannot survive into a response whose policy the platform is deciding.

Three handler tests read the policy header directly and now accept either name — they assert nonce alignment, not enforcement mode.

Verification

Full unit suite green: 3811 passed, 27995 steps, 0 failed. deno check, lint, fmt clean.

Summary by CodeRabbit

  • New Features

    • Content Security Policy can now be delivered in enforcement or report-only mode.
    • Explicit configuration and environment settings can enable CSP enforcement.
    • Security header handling recognizes both CSP header variants and prevents conflicting project-provided CSP headers.
  • Tests

    • Expanded coverage for development defaults, configuration options, environment overrides, empty policies, and enforcement/report-only parity.
    • Updated nonce and policy checks to support either CSP delivery mode.
  • Documentation

    • Updated security API reference links.

#3417 shipped the platform CSP floor enforced. It blocked, on every hosted
project that loads an asset the platform does not itself emit, every one of
those assets at once. A fleet audit finds ~100 projects in that state --
stock photography (images.unsplash.com, i.pravatar.cc, picsum.photos), video
(player.vimeo.com, vjs.zencdn.net), object storage (storage.googleapis.com),
embeds (www.youtube.com) -- including veryfront's own marketing site. Only two
projects, codersociety and tomcode, declare `security.csp` at all.

The owner gets no signal beyond a browser console, and no remedy short of a
republish: `security.csp` lives in project config, and a hosted project serves
config from its deployed release, so editing it changes nothing until the next
publish. codersociety hit exactly this -- added the three directives it needed
at 04:57, still served the bare floor hours later because its production
release was cut in June.

So the floor reports before it enforces. A project that has declared
`security.csp` has demonstrably looked at its policy and keeps the enforced
header; everyone else gets `Content-Security-Policy-Report-Only`, which
surfaces identical violations without breaking the page. The reported policy
is byte-identical to the enforced one, so what a project sees now is exactly
what it will get later -- there is a test pinning that.

Two escape hatches stay enforced: `VERYFRONT_CSP_ENFORCE` flips the default
once adoption is high enough, and a full `VERYFRONT_CSP` override is enforced
because writing one by hand is already an explicit ops act.

An empty `csp: {}` counts as opt-in. The signal is that the project touched the
key, not how much it put in it; reading emptiness as "unconfigured" would strand
a project that deliberately accepted the floor in report-only forever.

This is a narrow, time-boxed relaxation, not a retreat: the floor was enforced
for roughly one day, so report-only restores the state the fleet was already
in rather than weakening a settled protection.

`content-security-policy-report-only` joins the policy-owned header list, so a
project-provided value cannot survive into a response whose policy the platform
is deciding.

Verified: full unit suite green -- 3811 passed, 27995 steps, 0 failed. Three
handler tests read the policy header directly and now accept either name; they
assert nonce alignment, not enforcement mode. deno check, lint and fmt clean.
@kojiwakayama
kojiwakayama requested a review from kwakayama as a code owner August 7, 2026 12:04
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kojiwakayama, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c85b6070-c635-4170-a4f2-0d09ca7485bf

📥 Commits

Reviewing files that changed from the base of the PR and between 3f7c2e9 and 901441d.

📒 Files selected for processing (3)
  • docs/api-reference/veryfront/security.md
  • src/security/http/response/security-handler.test.ts
  • src/security/http/response/security-handler.ts
📝 Walkthrough

Walkthrough

The security handler now emits generated CSP as report-only by default and as enforced CSP when configuration or environment overrides require enforcement. Tests accept either CSP header and cover both delivery modes.

Changes

CSP delivery modes

Layer / File(s) Summary
CSP header selection and emission
src/security/http/response/security-handler.ts
The handler owns both CSP header names. It selects enforced CSP for configured or overridden CSP and report-only CSP otherwise.
Project header filtering
src/security/http/response/security-handler.ts
The handler ignores project-provided CSP headers and logs a warning. Other configured security headers remain effective.
CSP behavior validation
src/security/http/response/security-handler.test.ts, src/server/handlers/request/*.test.ts, tests/integration/server/production-server.test.ts
Tests cover header ownership, frame ancestors, development behavior, configuration and environment enforcement, policy filtering, policy parity, and nonce lookup from either CSP header.
Security API source references
docs/api-reference/veryfront/security.md
The documentation references the updated source locations for security-header functions.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant CSPHeaderSelection
  participant ApplySecurityHeaders
  participant Response

  Request->>CSPHeaderSelection: Provide configuration and environment state
  CSPHeaderSelection-->>ApplySecurityHeaders: Select enforced or report-only header
  ApplySecurityHeaders->>Response: Emit generated CSP policy
Loading

Possibly related PRs

Suggested reviewers: kwakayama, ariskemper

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: report the platform CSP floor before enforcing it.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/csp-floor-report-only-until-opt-in

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/security/http/response/security-handler.ts`:
- Around line 26-29: Update the extraHeaders loop in applySecurityHeaders to
skip both case-insensitive Content-Security-Policy and
Content-Security-Policy-Report-Only names, preventing project values from
replacing generated CSP headers or their delivery mode. Add regression tests
covering a custom value for each header name and verify the generated platform
policy remains effective.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f33d7b77-41bb-4c85-aff0-4be22176bb68

📥 Commits

Reviewing files that changed from the base of the PR and between 375412a and 69ada22.

📒 Files selected for processing (5)
  • src/security/http/response/security-handler.test.ts
  • src/security/http/response/security-handler.ts
  • src/server/handlers/request/openapi-docs.handler.test.ts
  • src/server/handlers/request/rsc/index.test.ts
  • src/server/handlers/request/static.handler.test.ts

Comment thread src/security/http/response/security-handler.ts
Three follow-ups to the same behaviour change, all found by CI rather than by
the unit suite -- integration tests are not part of `test:unit`, which is what
was run locally.

Four assertions in the production-server integration tests read
`content-security-policy` directly. Their fixtures declare no `security.csp`,
so the floor now reaches them report-only. They assert policy content and
nonce alignment, not enforcement mode, so they read whichever header carries
the policy via a local `readCsp` helper. The mode itself is covered by
dedicated cases in the unit tests.

`cspHeaderName` is no longer exported. Nothing outside this module calls it --
it was exported only out of habit, and exporting it widened the public API
surface and the generated reference for no gain.

Regenerate `docs/api-reference/veryfront/security.md`: adding the enforcement
helper shifted the line pins the reference cites. Regenerated with the same
Deno the CI check pins (2.7.7), never hand-edited.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/integration/server/production-server.test.ts`:
- Around line 34-37: Update the helper comment above the served-policy logic to
document that VERYFRONT_CSP and VERYFRONT_CSP_ENFORCE may select enforced
delivery even when fixtures omit security.csp, while preserving the note that
assertions validate policy content and nonce alignment rather than delivery
mode.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d7f9906-93f1-4423-9556-3b73b5c15100

📥 Commits

Reviewing files that changed from the base of the PR and between 69ada22 and 72d5336.

📒 Files selected for processing (3)
  • docs/api-reference/veryfront/security.md
  • src/security/http/response/security-handler.ts
  • tests/integration/server/production-server.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/security/http/response/security-handler.ts

Comment thread tests/integration/server/production-server.test.ts Outdated
Review catch on the report-only change, and a pre-existing hole it would have
widened.

`applySecurityHeaders` ends by writing every `security.headers` entry straight
onto the response. Every other name that loop can touch has a deliberate
override path through `getHeaderOverride` -- Referrer-Policy, X-Frame-Options,
and so on -- so re-setting them is the intended behaviour. CSP has no such
path: it is computed from the platform floor merged with `security.csp`, and a
value arriving through `security.headers` silently replaced the whole policy.
That was already true before this branch; adding a second delivery name meant a
project could also flip whether the policy is enforced or merely reported.

Skip both CSP names, case-insensitively, and warn once when one is dropped so
the project learns where the policy actually comes from.

`isSecurityPolicyResponseHeaderName` is deliberately not reused for the check.
It covers the whole policy-owned set, including the headers this loop is still
meant to override; matching on it would silently remove working overrides.
Worth noting that helper currently has no call sites at all, so the "server
integrations must remove project-provided values" contract its doc comment
describes is not enforced anywhere -- out of scope here, but it is why this had
to be handled at the point of use.

Three tests cover it: a project CSP value is ignored while its `security.csp`
still merges, the report-only name is ignored case-insensitively, and the
legitimate overrides plus arbitrary custom headers still land.

Also correct the integration helper comment: `VERYFRONT_CSP` and
`VERYFRONT_CSP_ENFORCE` select enforced delivery too, not just a `security.csp`
declaration.

Verified: 80 steps in the security-handler suite, 0 failed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/security/http/response/security-handler.test.ts`:
- Around line 809-820: Update the test case in the “ignores a project-supplied
report-only header, matching case-insensitively” block to use a mixed-case
Content-Security-Policy-Report-Only key instead of the all-lowercase form, while
preserving the existing assertions.

In `@src/security/http/response/security-handler.ts`:
- Around line 425-429: Update applySecurityHeaders so the ignoredCspHeader
warning is emitted only once, reusing the existing reserved-CORS warning
suppression pattern and state rather than logging on every response. Preserve
the current warning message and CSP detection behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c337cfa1-34b3-416c-8213-8395926fe56c

📥 Commits

Reviewing files that changed from the base of the PR and between 72d5336 and 3f7c2e9.

📒 Files selected for processing (3)
  • src/security/http/response/security-handler.test.ts
  • src/security/http/response/security-handler.ts
  • tests/integration/server/production-server.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/integration/server/production-server.test.ts

Comment thread src/security/http/response/security-handler.test.ts
Comment thread src/security/http/response/security-handler.ts Outdated
Adding the enforcement helper and the CSP header guard shifted the line pins
the generated reference cites. Regenerated with the Deno the CI check pins
(2.7.7); never hand-edited.
…e-insensitively

Two review catches, both valid.

`applySecurityHeaders` runs once per response, so the warning for an ignored
`security.headers` CSP entry fired on every request a misconfigured project
served. Guarded with the same WeakSet suppression the reserved-CORS warning
next to it already uses, keyed on the headers object so one config warns once.

The report-only test used a lowercase key, which is already the form the
lookup normalizes to -- a regression that dropped case-normalization would
have passed it. Switched to `Content-Security-Policy-Report-Only` so the test
exercises the contract it claims to.

Docs regenerated: the suppression changed line pins in the generated API
reference again.

80 steps, 0 failed.
@kojiwakayama
kojiwakayama added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 7b6ec3a Aug 7, 2026
31 checks passed
@kojiwakayama
kojiwakayama deleted the fix/csp-floor-report-only-until-opt-in branch August 7, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant